-- 
-- Please see the readme.txt file included with this distribution for 
-- attribution and copyright information.
--

--
--  DATA STRUCTURES
--

-- rCreature
--		sType
--		sName
--		nodeCreature
--		sCreatureNode
--		nodeCT
-- 		sCTNode
	
--	rAbility
--		type = "attack", "damage", "heal"
--		name = ""
--		order = # (optional)
--		range = "M", "R", "C", "A", "Z", "", all 2-way combinations
--		defense = "" (attack only)
--		clauses = {};

--	rAbilityClause
-- 		stat = {}
--		mod = # (attack only)
-- 		basemult = # (damage/heal only)
--		dicestr = "" (damage/heal only)
--		critdicestr = "" (damage only)
--		cost = # (heal only)
--		subtype = "" (damage/heal only)

--	rFocus
--		type = "attack", "damage"
--		name = ""
--		range = "M", "R"
--		clauses = {}
--		defense = "" (attack only)
--		properties = "" (damage only)
--		basedice = {} (damage only)
--		critdice = {} (damage only)
--		critmod = # (damage only)
--		crittype = "" (damage only)

--	rFocusClause
--		stat = {}
--		mod = #
--		dicestr = "" (damage only)
--		subtype = "" (damage only)

-- rEffect
--		sName = ""
--		sExpire = ""
-- 		nInit = #
--		nSaveMod = #
--		sSource = ""
--		nGMOnly = 0, 1
--		sApply = "", "once", "single"

--
--  GENERAL ACTIONS SUPPORT
--  (DRAG AND DOUBLE-CLICK)
--

function dragAction(draginfo, actiontype, val, desc, rSourceActor, dice, isOverrideUserSetting)
	-- HANDLE HIDDEN ROLLS
	if User.isHost() and OptionsManager.isOption("REVL", "off") then
		local isGMOnly = string.match(desc, "^%[GM%]");
		local isDiceTower = string.match(desc, "^%[TOWER%]");
		if not isGMOnly and not isDiceTower then
			desc = "[GM] " .. desc;
		end
	end
	
	-- Make sure we're on slot 1
	draginfo.setSlot(1);

	-- Set the drag type based on user settings and the information passed
	if OptionsManager.isOption("DRGR", "on") or isOverrideUserSetting then
		draginfo.setType(actiontype);

		-- If empty dice set, then make it a d0 to hide GM rolls
		-- If no dice variable defined, then we want to use the default die (d20)
		if dice then
			if #dice == 0 then
				draginfo.setDieList({ "d0" });
			else
				draginfo.setDieList(dice);
			end
		else
			draginfo.setDieList({ "d20" });
		end
	else
		draginfo.setType("number");
	end

	-- Set the description and modifier value
	draginfo.setDescription(desc);
	draginfo.setNumberData(val);

	-- If we have a node reference, then use it
	if rSourceActor then
		if rSourceActor.sCTNode ~= "" then
			draginfo.setShortcutData("combattracker_entry", rSourceActor.sCTNode);
		elseif rSourceActor.sCreatureNode ~= "" then
			if rSourceActor.sType == "pc" then
				draginfo.setShortcutData("charsheet", rSourceActor.sCreatureNode);
			elseif rSourceActor.sType == "npc" then
				draginfo.setShortcutData("npc", rSourceActor.sCreatureNode);
			end
		end
	end

	-- We've handled this drag scenario
	return true;
end

function applyMarkPenalty(rAttacker, aTargets)
	local bApplyMarkPenalty = false;
	if #aTargets > 0 then
		-- APPLY MARKING PENALTY, IF NEEDED
		local aMarkEffects = EffectsManager.hasEffectCondition(rAttacker.nodeCT, "Marked");
		if aMarkEffects then
			for k,v in pairs(aMarkEffects) do
				local bEffectApplyMarkPenalty = false;
				
				local sEffectSourceNode = NodeManager.get(v, "source_name", "");
				if sEffectSourceNode ~= "" then
					bEffectApplyMarkPenalty = true;

					for keyActorTarget, sActorTargetNode in pairs(aTargets) do
						if sEffectSourceNode == sActorTargetNode then
							bEffectApplyMarkPenalty = false;
						end
					end
				end
				
				if bEffectApplyMarkPenalty then
					bApplyMarkPenalty = true;
				end
			end
		end
	end
	
	return bApplyMarkPenalty;
end

function dclkAction(actiontype, val, desc, rSourceActor, rTargetActor, dice, isOverrideUserSetting, other_custom)
	-- Decide what to do based on user settings
	local opt_dclk = OptionsManager.getOption("DCLK");
	if opt_dclk ~= "on" and not isOverrideUserSetting then
		if opt_dclk == "mod" then
			ModifierStack.addSlot(desc, val);
		end
		return true;
	end

	-- HANDLE HIDDEN ROLLS
	if User.isHost() and OptionsManager.isOption("REVL", "off") then
		local isGMOnly = string.match(desc, "^%[GM%]");
		local isDiceTower = string.match(desc, "^%[TOWER%]");
		if not isGMOnly and not isDiceTower then
			desc = "[GM] " .. desc;
		end
	end
	
	-- If we have a node reference, then use it
	local custom = CombatCommon.buildCustomRollArray(rSourceActor, rTargetActor);
	if other_custom then
		for k,v in pairs(other_custom) do
			custom[k] = v;
		end
	end
	
	-- If empty dice set, then make it a d0 to hide GM rolls
	-- If no dice variable defined, then we want to use the default die (d20)
	if dice then
		if #dice == 0 then
			dice = { "d0" };
		end
	else
		 dice = { "d20" };
	end
	
	-- SPECIAL TARGET HANDLING
	local bActionHandled = false;
	if rTargetActor then
		if actiontype == "attack" then
			if rTargetActor.nodeCT then
				-- APPLY MARKING PENALTY, IF NEEDED
				if applyMarkPenalty(rSourceActor, {rTargetActor.sCTNode}) then
					val = val - 2;
					desc = desc .. " [MARK -2]";
				end
			end
		end
	else
		if actiontype == "attack" then
			-- DETERMINE TARGETS
			local sTargetType, aTargets = TargetingManager.getTargets(rSourceActor);

			-- APPLY MARKING PENALTY, IF NEEDED
			if applyMarkPenalty(rSourceActor, aTargets) then
				val = val - 2;
				desc = desc .. " [MARK -2]";
			end
			
			-- SEPARATE ATTACKS FOR EACH TARGET
			if #aTargets == 1 then
				custom[sTargetType] = aTargets[1];
			elseif #aTargets > 1 then
				ModifierStack.setLockCount(#aTargets);
				custom[sTargetType] = table.concat(aTargets, ChatManager.SPECIAL_MSG_SEP);
				for keyTarget, sTargetNode in pairs(aTargets) do
					local sAttackDesc = desc .. " [TARGET " .. keyTarget .. "]";
					ChatManager.DieControlThrow(actiontype, val, sAttackDesc, custom, dice);
				end
				bActionHandled = true;
			end

		elseif actiontype == "damage" then
			-- DETERMINE TARGETS
			local sTargetType, aTargets = TargetingManager.getTargets(rSourceActor);

			-- IF CLOSE/AREA RANGE DAMAGE ROLL, THEN USE ONE DAMAGE ROLL FOR ALL TARGETS
			local sRange = string.match(desc, "%[DAMAGE %((%w)%)%]") or "";
			if sRange == "C" or sRange == "A" then
				if #aTargets > 0 then
					custom[sTargetType] = table.concat(aTargets, ChatManager.SPECIAL_MSG_SEP);
				end

			-- OTHERWISE, USE ONE DAMAGE ROLL PER TARGET
			else
				if #aTargets == 1 then
					custom[sTargetType] = aTargets[1];
				elseif #aTargets > 1 then
					ModifierStack.setLockCount(#aTargets);
					custom[sTargetType] = table.concat(aTargets, ChatManager.SPECIAL_MSG_SEP);
					for keyTarget, sTargetNode in pairs(aTargets) do
						local sDamageDesc = desc .. " [TARGET " .. keyTarget .. "]";
						ChatManager.DieControlThrow(actiontype, val, sDamageDesc, custom, dice);
					end
					bActionHandled = true;
				end
			end
		end
	end
	
	if not bActionHandled then
		-- BASIC THROW THE DICE
		ChatManager.DieControlThrow(actiontype, val, desc, custom, dice);
	end
	
	-- We've handled this double-click scenario
	return true;
end


--
--  EFFECT SUPPORT
--  (DRAG, DOUBLE-CLICK, ENCODE/DECODE)
--

function getEffectTargets(rActor, rEffect)
	local sTargetType, aTargets = TargetingManager.getTargets(rActor, rEffect);

	if sTargetType == "targetpc" then
		aTargets[1] = CombatCommon.getCTFromNode(aTargets[1]);
		if not aTargets[1] then
			ChatManager.SystemMessage("[ERROR] Self-targeting of effects requires PC to be added to combat tracker.");
			sTargetType = "";
		end
	end
	
	return sTargetType, aTargets;
end

function dragEffect(draginfo, rActor, rEffect)
	encodeEffectForDrag(draginfo, rActor, rEffect);
	return true;
end

function dclkEffect(rActor, rEffect, rTargetActor)
	local sTargetType = "targetct";
	local aTargets = {};
	if rTargetActor and rTargetActor.nodeCT then
		table.insert(aTargets, rTargetActor.sCTNode);
	else
		sTargetType, aTargets = getEffectTargets(rActor, rEffect);
	end

	if #aTargets > 0 then
		for keyTarget, sTargetNode in pairs(aTargets) do
			ChatManager.sendSpecialMessage(ChatManager.SPECIAL_MSGTYPE_APPLYEFF, 
					{sTargetNode, 
					rEffect.sName or "", 
					rEffect.sExpire or "", 
					rEffect.nSaveMod or 0, 
					rEffect.nInit or 0, 
					rEffect.sSource or "", 
					rEffect.nGMOnly or 0, 
					rEffect.sApply or ""});
		end
	else
		ChatManager.reportEffect(rEffect);
	end

	return true;
end

function encodeEffectAsText(rEffect, sTarget)
	local aMessage = {};
	
	if rEffect then
		if rEffect.nGMOnly == 1 then
			table.insert(aMessage, "[GM]");
		end
		table.insert(aMessage, "[EFFECT] " .. rEffect.sName .. " EXPIRES " .. rEffect.sExpire);
		if rEffect.nInit and StringManager.isWord(rEffect.sExpire, {"endnext", "start", "end" }) then
			table.insert(aMessage, "[INIT " .. rEffect.nInit .. "]");
		end
		if rEffect.sApply and rEffect.sApply ~= "" then
			table.insert(aMessage, "[" .. string.upper(rEffect.sApply) .. "]");
		end
		if sTarget then
			table.insert(aMessage, "[to " .. sTarget .. "]");
		end
		if rEffect.sSource and rEffect.sSource ~= "" then
			table.insert(aMessage, "[by " .. NodeManager.get(DB.findNode(rEffect.sSource), "name", "") .. "]");
		end
	end
	
	return table.concat(aMessage, " ");
end

function encodeEffectForDrag(draginfo, rActor, rEffect)
	if rEffect and rEffect.sName ~= "" then
		draginfo.setType("effect");
		draginfo.setDescription(rEffect.sName);

		draginfo.setSlot(1);
		draginfo.setStringData(rEffect.sName);
		draginfo.setNumberData(rEffect.nSaveMod);

		draginfo.setSlot(2);
		draginfo.setStringData(rEffect.sExpire);
		draginfo.setNumberData(rEffect.nInit or 0);

		draginfo.setSlot(3);
		draginfo.setStringData(rEffect.sSource or "");
		draginfo.setNumberData(rEffect.nGMOnly or 0);
		
		draginfo.setSlot(4);
		draginfo.setStringData(rEffect.sApply or "");

		draginfo.setSlot(5);
		if Input.isShiftPressed() then
			local sTargetType, aTargets = getEffectTargets(rActor);
			draginfo.setStringData(table.concat(aTargets, "|"));
		end
		
	end
end

function decodeEffectFromDrag(draginfo)
	local rEffect = nil;
	
	if draginfo.getType() == "effect" then
		rEffect = {};

		draginfo.setSlot(1);
		rEffect.sName = draginfo.getStringData();
		rEffect.nSaveMod = draginfo.getNumberData();
		draginfo.setSlot(2);
		rEffect.sExpire = draginfo.getStringData();
		local nTempInit = draginfo.getNumberData() or 0;
		draginfo.setSlot(3);
		local sEffectSource = draginfo.getStringData();
		if sEffectSource and sEffectSource ~= "" then
			rEffect.sSource = sEffectSource;
			rEffect.nInit = nTempInit;
		else
			rEffect.sSource = "";
			rEffect.nInit = 0;
		end
		rEffect.nGMOnly = draginfo.getNumberData() or 0;

		draginfo.setSlot(4);
		local sApply = draginfo.getStringData();
		if sApply and sApply ~= "" then
			rEffect.sApply = sApply;
		else
			rEffect.sApply = "";
		end
		
		draginfo.setSlot(5);
		local sThirdPartyTargets = draginfo.getStringData();
		if sThirdPartyTargets and sThirdPartyTargets ~= "" then
			rEffect.targets = StringManager.split(sThirdPartyTargets, "|");
		end
		
	elseif draginfo.getType() == "number" then
		local sDesc = draginfo.getDescription();
		local sEffectName, sEffectExpire = string.match(sDesc, "%[EFFECT%] (.+) EXPIRES ?(%a*)");
		if sEffectName and sEffectExpire then
			rEffect = {};
			
			rEffect.sName = sEffectName;
			rEffect.nSaveMod = draginfo.getNumberData();
			rEffect.sExpire = sEffectExpire;
			
			rEffect.sSource = "";
			local sEffectInit = string.match(sDesc, "%[INIT (%d+)%]");
			if sEffectInit then
				rEffect.nInit = tonumber(sEffectInit) or 0;
			else
				rEffect.nInit = 0;
			end

			if string.match(sDesc, "%[GM%]") then
				rEffect.nGMOnly = 1;
			else
				rEffect.nGMOnly = 0;
			end

			if string.match(sDesc, "%[ONCE%]") then
				rEffect.sApply = "once";
			elseif string.match(sDesc, "%[SINGLE%]") then
				rEffect.sApply = "single";
			else
				rEffect.sApply = "";
			end
		end
	end
	
	return rEffect;
end


--
--  ROLL FUNCTIONS
--  (ATTACK, DAMAGE, HEAL)
--

function buildDamageRoll(rCreature, rAbility, rFocus)
	-- SETUP
	local notify_tags = {};
	local dice = {};
	local mod = 0;
	local rPrimary = rAbility;
	if not rPrimary then
		rPrimary = rFocus;
	end
	
	-- IS CRITICAL?
	local isCritical = Input.isShiftPressed();
	local max_dice_results = {};

	-- IS VORPAL?
	local isVorpal = false;
	if rFocus then
		if string.match(rFocus.properties, "vorpal") then
			isVorpal = true;
		end
	end
	
	-- IS BRUTAL?
	local isBrutal = 0;
	local brutal_dice_results = {};
	if rFocus then
		local brutal_str = string.match(rFocus.properties, "brutal (%d+)");
		isBrutal = tonumber(brutal_str) or 0;
	end

	-- BUILD DAMAGE FILTER
	local aDamageFilter = {};
	if rPrimary then
		if rPrimary.range == "M" then
			table.insert(aDamageFilter, "melee");
		elseif rPrimary.range == "M" then
			table.insert(aDamageFilter, "ranged");
		elseif rPrimary.range == "C" then
			table.insert(aDamageFilter, "close");
		elseif rPrimary.range == "A" then
			table.insert(aDamageFilter, "area");
		end
	end
	
	-- MAKE SURE WE HAVE CLAUSES AND THEN ITERATE
	local aDiceDamageType = {};
	local dmgtype_results = {};
	local dmgtype_first = nil;
	local nDamageEffectCount = 0;
	if rPrimary and rPrimary.clauses then
		-- ITERATE THROUGH EACH DAMAGE CLAUSE
		for k, v in pairs(rPrimary.clauses) do

			-- GET THE DAMAGE TYPE FOR THIS CLAUSE
			local clause_dicestart = #dice;
			local clause_modstart = mod;
			local clause_dmgtype = { v.subtype };

			-- IF POWER, THEN START WITH WEAPON DICE AND DAMAGE TYPE
			if rAbility and rFocus and v.basemult > 0 then

				-- CHECK FOR WEAPON MULTIPLE BONUS EFFECTS
				local mult = v.basemult;
				if rCreature then
					local nDmgWeaponMod, nEffectSubCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"DMGW"}, true, aDamageFilter);
					if nEffectSubCount > 0 then
						mult = mult + nDmgWeaponMod;
						nDamageEffectCount = nDamageEffectCount + nEffectSubCount;
					end
				end

				-- IF CRITICAL AND WEAPON POWER, MAXIMIZE FOCUS DICE
				if isCritical then
					local max_dice_count = #(rFocus.basedice) * mult;
					for i = #dice + 1, #dice + max_dice_count do
						max_dice_results[i] = true;
					end
				end

				-- IF CRITICAL AND WEAPON POWER, CHECK BRUTAL
				if isBrutal > 0 then
					local brutal_dice_count = #(rFocus.basedice) * mult;
					for i = #dice + 1, #dice + brutal_dice_count do
						brutal_dice_results[i] = true;
					end
				end

				-- ADD BASE WEAPON DAMAGE TYPE
				if rFocus.subtype ~= "" then
					table.insert(clause_dmgtype, rFocus.subtype);
				end

				-- ADD BASE WEAPON DICE
				for i = 1, mult do
					for i, die in ipairs(rFocus.basedice) do
						table.insert(dice, die);
					end
				end
			end

			-- ADD PRIMARY ABILITY MODIFIERS
			for stat_key, stat_val in pairs(v.stat) do
				mod = mod + getAbilityBonus(rCreature, stat_val);
			end

			-- GET DICE AND MODIFIERS.  IF CRITICAL AND NO SPECIAL CRIT DEFINED, THEN MAXIMIZE
			local powerdice = {};
			local powermod = 0;
			if isCritical then
				if rAbility and v.critdicestr ~= "" then
					powerdice, powermod = StringManager.convertStringToDice(v.critdicestr);
				else
					powerdice, powermod = StringManager.convertStringToDice(v.dicestr);
					for i = #dice + 1, #dice + #powerdice do
						max_dice_results[i] = true;
					end
				end
			else
				powerdice, powermod = StringManager.convertStringToDice(v.dicestr);
			end

			-- IF CRITICAL AND BASIC WEAPON DAMAGE, CHECK BRUTAL
			if rFocus and not rAbility and isBrutal > 0 then
				for i = #dice + 1, #dice + #powerdice do
					brutal_dice_results[i] = true;
				end
			end

			-- ADD PRIMARY DICE AND MODIFIERS
			for i, die in ipairs(powerdice) do
				table.insert(dice, die);
			end
			mod = mod + powermod;

			-- ADD TO THE DAMAGE TYPES FOR THIS SET
			if ((#dice - clause_dicestart) > 0) or ((mod - clause_modstart) ~= 0) then
				table.insert(dmgtype_results, {diecount = #dice - clause_dicestart, mod = mod - clause_modstart, dmgtype = table.concat(clause_dmgtype, ",")});
			end
			if not dmgtype_first then
				dmgtype_first = table.concat(clause_dmgtype, ",");
			end
		end
	end
	if not dmgtype_first then
		dmgtype_first = "";
	end

	-- IS STATIC?
	-- STATIC DAMAGE (IGNORE EFFECTS AND SPECIAL CRITICAL DICE)
	local isStatic = (#dice == 0);

	-- STANDARD DAMAGE
	local aDamageEffectDice = {};
	local nDamageEffectMod = 0;
	if not isStatic then
		
		-- TRACK ADDITIONAL UNTYPED MODIFIERS
		local add_dicestart = #dice;
		local add_modstart = mod;
		
		-- GET FOCUS MODIFIERS
		if rAbility and rFocus then
			for k, v in pairs(rFocus.clauses) do
				mod = mod + v.mod;
			end
		end
		
		-- GET EFFECT MODIFIERS
		local eff_results = {};
		if rCreature then
			local nEffectSubCount = 0;
			eff_results, nEffectSubCount = EffectsManager.getEffectsBonusByType(rCreature.nodeCT, {"DMG"}, true, aDamageFilter);
			nDamageEffectCount = nDamageEffectCount + nEffectSubCount;
		end

		-- ADD UNTYPED EFFECT DICE AND MODIFIERS
		local max_effdice_count = 0;
		local max_effdice_start = #dice;
		local other_eff_results = {};
		for k, v in pairs(eff_results) do
			if StringManager.contains(DataCommon.dmgtypes, k) then
				table.insert(other_eff_results, {dice = v.dice, mod = v.mod, dmgtype = k});
			else
				max_effdice_count = max_effdice_count + #(v.dice);
				for k2, v2 in pairs(v.dice) do
					table.insert(dice, v2);
				end
				mod = mod + v.mod;
			end

			for k2, v2 in pairs(v.dice) do
				table.insert(aDamageEffectDice, v2);
			end
			nDamageEffectMod = nDamageEffectMod + v.mod
		end

		-- TRACK DAMAGE TYPE FOR BASE POWER DAMAGE AND UNTYPED EFFECTS
		if ((#dice - add_dicestart) > 0) or ((mod - add_modstart) ~= 0) then
			table.insert(dmgtype_results, {diecount = #dice - add_dicestart, mod = mod - add_modstart, dmgtype = dmgtype_first});
		end

		-- ADD IN TYPED EFFECT DICE AND MODIFIERS
		for k,v in pairs(other_eff_results) do
			max_effdice_count = max_effdice_count + #(v.dice);
			for k2, v2 in pairs(v.dice) do
				table.insert(dice, v2);
			end
			mod = mod + v.mod;
			if (#(v.dice) > 0) or (v.mod ~= 0) then
				table.insert(dmgtype_results, {diecount = #(v.dice), mod = v.mod, dmgtype = v.dmgtype});
			end
		end
		
		-- IF CRITICAL, MAXIMIZE EFFECT DICE
		if isCritical then
			for i = max_effdice_start + 1, max_effdice_start + max_effdice_count do
				max_dice_results[i] = true;
			end
		end

		-- HANDLE VORPAL FOCUS PROPERTY
		if isVorpal then
			table.insert(notify_tags, "[VORPAL]");
		end
		
		-- APPLY SPECIAL CRITICAL HANDLING (BESIDES MAXIMIZED ROLLS WHICH ARE ALREADY DONE)
		if isCritical then
			-- HANDLE HIGH CRIT FOCI
			local bRollHighCrit = false;
			local bFocusHighCrit = (rFocus and string.match(rFocus.properties, "high crit"));
			if rAbility and rAbility.clauses then
				for k, v in pairs(rAbility.clauses) do
					if v.basemult > 0 then
						bRollHighCrit = bFocusHighCrit;
					end
				end
			else
				bRollHighCrit = bFocusHighCrit;
			end

			if bRollHighCrit then
				-- ADD HIGH CRIT TAG
				table.insert(notify_tags, "[HIGH CRIT]");

				-- DETERMINE HIGH CRIT MULTIPLE (BASED ON LEVEL)
				local highcritmult = 1;
				local creature_level = getAbilityBonus(rCreature, "LEV");
				if creature_level > 20 then
					highcritmult = 3;
				elseif creature_level > 10 then
					highcritmult = 2;
				end

				if isBrutal > 0 then
					local brutal_dice_count = #(rFocus.basedice) * highcritmult;
					for i = #dice + 1, #dice + brutal_dice_count do
						brutal_dice_results[i] = true;
					end
				end

				-- ADD HIGH CRIT DICE AND DAMAGE TYPE
				local dmgtype_entry = {diecount = 0, mod = 0, dmgtype = dmgtype};
				for i = 1, highcritmult do
					for k,v in ipairs(rFocus.basedice) do
						table.insert(dice, v);
					end
					dmgtype_entry.diecount = dmgtype_entry.diecount + #(rFocus.basedice);
				end
				if dmgtype_entry.diecount > 0 then
					dmgtype_entry.dmgtype = dmgtype_first;
					table.insert(dmgtype_results, dmgtype_entry);
				end
			end
			
			-- GET WEAPON CRITICAL DICE
			if rFocus then
				for k, v in pairs(rFocus.critdice) do
					table.insert(dice, v);
				end
				mod = mod + rFocus.critmod;
				if (#(rFocus.critdice) > 0) or (rFocus.critmod ~= 0) then
					table.insert(dmgtype_results, 
							{diecount = #(rFocus.critdice), mod = rFocus.critmod, dmgtype = rFocus.crittype});
				end
			end
		end
		
		-- ADD BRUTAL TAG
		if isBrutal > 0 and #brutal_dice_results > 0 then
			local brutal_dice_output = {};
			local i = 1;
			while i <= #dice do
				if brutal_dice_results[i] then
					local j = i + 1;
					while brutal_dice_results[j] do
						j = j + 1;
					end

					if (i == 1) and (j > #dice) then
						-- SKIP
					elseif i == j - 1 then
						table.insert(brutal_dice_output, "" .. i);
					else
						table.insert(brutal_dice_output, "" .. i .. "-" .. (j - 1));
					end
					i = j - 1;
				end
				i = i + 1;
			end
			if #brutal_dice_output > 0 then
				table.insert(notify_tags, "[BRUTAL " .. isBrutal .. " (D" .. table.concat(brutal_dice_output, ",") .. ")]");
			else
				table.insert(notify_tags, "[BRUTAL " .. isBrutal .. "]");
			end
		end
		
		-- ADD MAX TAG
		if isCritical and #max_dice_results > 0 then
			local max_dice_output = {};
			local i = 1;
			while i <= #dice do
				if max_dice_results[i] then
					local j = i + 1;
					while max_dice_results[j] do
						j = j + 1;
					end

					if (i == 1) and (j > #dice) then
						-- SKIP
					elseif i == j - 1 then
						table.insert(max_dice_output, "" .. i);
					else
						table.insert(max_dice_output, "" .. i .. "-" .. (j - 1));
					end
					i = j - 1;
				end
				i = i + 1;
			end
			if #max_dice_output > 0 then
				table.insert(notify_tags, "[MAX (D" .. table.concat(max_dice_output, ",") .. ")]");
			else
				table.insert(notify_tags, "[MAX]");
			end
		end
		
		-- ADD CRITICAL TAG
		if isCritical then
			table.insert(notify_tags, "[CRITICAL]");
		end
	end

	-- CHECK FOR HALF DAMAGE EFFECTS
	if rCreature then
		if EffectsManager.hasEffect(rCreature.nodeCT, "Weakened") then
			table.insert(notify_tags, "[HALF]");
		end
	end
	
	-- IF ANY EFFECTS HAVE CONTRIBUTED, THEN ADD A NOTIFICATION TAG
	if nDamageEffectCount > 0 then
		local sMod = StringManager.convertDiceToString(aDamageEffectDice, nDamageEffectMod, true);
		if sMod ~= "" then
			table.insert(notify_tags, "[EFFECTS " .. sMod .. "]");
		else
			table.insert(notify_tags, "[EFFECTS]");
		end
	end
	
	-- ADD FOCUS NAME (IF OPTION ENABLED)
	if rAbility and rFocus and rFocus.name ~= "" and OptionsManager.isOption("SWPN", "on") then
		table.insert(notify_tags, "[USING " .. rFocus.name .. "]");
	end
	
	-- BUILD OUTPUT
	local s = "[DAMAGE";
	if rPrimary and rPrimary.order and rPrimary.order > 1 then
		s = s .. " #" .. rPrimary.order;
	end
	if StringManager.isWord(rPrimary.range, {"M", "R", "C", "A"}) then
		s = s .. " (" .. rPrimary.range .. ")";
	end
	s = s .. "]";
	if rPrimary then
		s = s .. " " .. rPrimary.name;
	end
	
	-- ADD DAMAGE TYPE TAGS
	local typecount = 0;
	local last_dmgtype = nil;
	local last_diecount = 0;
	local last_mod = 0;
	for i = 1, #dmgtype_results do
		if last_dmgtype and dmgtype_results[i].dmgtype ~= last_dmgtype then
			if last_dmgtype == "" then
				s = s .. " [TYPE: untyped"; 
			else
				s = s .. " [TYPE: " .. last_dmgtype; 
			end
			s = s .. " (" .. last_diecount .. "D";
			if last_mod > 0 then
				s = s .. "+" .. last_mod;
			elseif last_mod < 0 then
				s = s .. last_mod;
			end
			s = s .. ")]";

			typecount = typecount + 1;
			last_diecount = 0;
			last_mod = 0;
		end

		last_dmgtype = dmgtype_results[i].dmgtype;
		last_diecount = last_diecount + dmgtype_results[i].diecount;
		last_mod = last_mod + dmgtype_results[i].mod;
	end
	if last_dmgtype and last_dmgtype ~= "" then
		s = s .. " [TYPE: " .. last_dmgtype; 
		if typecount > 0 then
			s = s .. " (" .. last_diecount .. "D";
			if last_mod > 0 then
				s = s .. "+" .. last_mod;
			elseif last_mod < 0 then
				s = s .. last_mod;
			end
			s = s .. ")";
		end
		s = s .. "]";
	end
	
	-- ADD EXTRA TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildAttackRoll(rCreature, rAbility, rFocus)
	-- SETUP
	local notify_tags = {};
	local dice = { "d20" };
	local mod = 0;
	local rPrimary = rAbility;
	if not rPrimary then
		rPrimary = rFocus;
	end
	
	-- IS OPPORTUNITY ATTACK?
	local bOpportunity = Input.isShiftPressed();
	if bOpportunity then
		table.insert(notify_tags, "[OPPORTUNITY]");
	end

	-- HAS EXTRA CRIT RANGE?
	local nAltCritRange = 0;
	if rFocus then
		local sCritRange = string.match(rFocus.properties, "crit range (%d+)");
		nAltCritRange = tonumber(sCritRange) or 0;
		if nAltCritRange == 0 then
			if string.match(rFocus.properties, "jagged") then
				nAltCritRange = 19;
			end
		end
	end
	if nAltCritRange > 0 then
		table.insert(notify_tags, "[CRIT " .. nAltCritRange .. "]");
	end
	
	-- BUILD ATTACK FILTER
	local aAttackFilter = {};
	if rPrimary then
		if rPrimary.range == "M" then
			table.insert(aAttackFilter, "melee");
		elseif rPrimary.range == "M" then
			table.insert(aAttackFilter, "ranged");
		elseif rPrimary.range == "C" then
			table.insert(aAttackFilter, "close");
		elseif rPrimary.range == "A" then
			table.insert(aAttackFilter, "area");
		end
	end
	if bOpportunity then
		table.insert(aAttackFilter, "opportunity");
	end
	
	-- ADD ATTACK STATS
	if rPrimary then
		local statcount = 0;
		for k, v in pairs(rPrimary.clauses) do
			for k2, v2 in pairs(v.stat) do
				mod = mod + getAbilityBonus(rCreature, v2);
				statcount = statcount + 1;
			end
		end
		
		-- IF WE ARE ADDING ABILITY MODIFIER, THEN ALSO ADD LEVEL BONUS ONCE
		if statcount > 0 then
			mod = mod + math.floor(getAbilityBonus(rCreature, "LEV") / 2);
		end
	end
	
	-- ADD ATTACK MODIFIERS
	if rAbility then
		for k, v in pairs(rAbility.clauses) do
			mod = mod + v.mod;
		end
	end
	if rFocus then
		for k, v in pairs(rFocus.clauses) do
			mod = mod + v.mod;
		end
	end

	-- ADD CREATURE MODIFIER
	if rPrimary and rCreature and rCreature.sType == "pc" then
		if rPrimary.range == "R" then
			mod = mod + NodeManager.get(rCreature.nodeCreature, "attacks.ranged.misc", 0) +
				NodeManager.get(rCreature.nodeCreature, "attacks.ranged.temporary", 0);
		elseif rPrimary.range == "M" then
			mod = mod + NodeManager.get(rCreature.nodeCreature, "attacks.melee.misc", 0) +
				NodeManager.get(rCreature.nodeCreature, "attacks.melee.temporary", 0);
		end
	end
	
	-- GET EFFECT MODIFIERS
	local sEffects = "";
	if rCreature then
		local bEffects = false;

		-- GET ATTACK MODIFIERS
		local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"ATK"}, false, aAttackFilter);
		if (nEffectCount > 0) then
			bEffects = true;
		end
		
		-- GET CONDITION MODIFIERS
		if EffectsManager.hasEffect(rCreature.nodeCT, "CA") then
			eff_mod = eff_mod + 2;
			bEffects = true;
		elseif EffectsManager.hasEffect(rCreature.nodeCT, "Invisible") then
			eff_mod = eff_mod + 2;
			bEffects = true;
		end
		if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Prone") then
			eff_mod = eff_mod - 2;
			bEffects = true;
		end
		if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Restrained") then
			eff_mod = eff_mod - 2;
			bEffects = true;
		end

		-- GET OTHER EFFECT MODIFIERS
		if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Running") then
			eff_mod = eff_mod - 5;
			bEffects = true;
		end
		if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Squeezing") then
			eff_mod = eff_mod - 5;
			bEffects = true;
		end
		
		if bEffects then
			-- ADD IN THE MODIFIERS
			for k,v in pairs(eff_dice) do
				table.insert(dice, v);
			end
			mod = mod + eff_mod;

			-- ADD THE EFFECTS NOTIFICATION TAG
			local sEffects = "";
			local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
			if sMod ~= "" then
				sEffects = "[EFFECTS " .. sMod .. "]";
			else
				sEffects = "[EFFECTS]";
			end
			table.insert(notify_tags, sEffects);
		end
	end

	-- ADD FOCUS NAME (IF OPTION ENABLED)
	if rAbility and rFocus and rFocus.name ~= "" and OptionsManager.isOption("SWPN", "on") then
		table.insert(notify_tags, "[USING " .. rFocus.name .. "]");
	end
	
	-- BUILD THE OUTPUT
	local s = "[ATTACK";
	if rPrimary and rPrimary.order and rPrimary.order > 1 then
		s = s .. " #" .. rPrimary.order;
	end
	if StringManager.isWord(rPrimary.range, {"M", "R", "C", "A"}) then
		s = s .. " (" .. rPrimary.range .. ")";
	end
	s = s .. "]";
	if rPrimary then
		s = s .. " " .. rPrimary.name;
		if rPrimary.defense == "ac" then
			s = s .. " (vs. AC)";
		elseif rPrimary.defense == "fortitude" then
			s = s .. " (vs. Fort)";
		elseif rPrimary.defense == "reflex" then
			s = s .. " (vs. Ref)";
		elseif rPrimary.defense == "will" then
			s = s .. " (vs. Will)";
		else
			s = s .. " (vs. -)";
		end
	end
	
	-- ADD ALL OTHER TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildHealRoll(rCreature, rAbility)
	-- SETUP
	local notify_tags = {};
	local dice = {};
	local mod = 0;
	local healcost = 0;
	local hsvmult = 0;
	local healtype = "";
	
	-- ITERATE THROUGH THE CLAUSES
	for k, v in pairs(rAbility.clauses) do
		-- GET POWER BASE DICE AND MODIFIER
		local clausedice, clausemod = StringManager.convertStringToDice(v.dicestr);
		for i, die in pairs(clausedice) do
			table.insert(dice, die);
		end
		mod = mod + clausemod;

		-- GET POWER ABILITY MODIFIERS
		for k,v in pairs(v.stat) do
			mod = mod + getAbilityBonus(rCreature, v);
		end
		
		-- ADD IN ANY HEALING SURGE COSTS
		healcost = healcost + v.cost;
		
		-- ADD IN ANY HEALING SURGE VALUE MULTIPLIERS
		hsvmult = hsvmult + v.basemult;
		
		-- FOR HEALING, JUST TAKE THE LAST TYPE
		healtype = v.subtype;
	end
	
	-- Add the power's healing cost to the output
	if healcost ~= 0 then
		table.insert(notify_tags, "[COST " .. healcost .. "]");
	end
	
	-- Add the power's healing surge value bonus
	if hsvmult ~= 0 then
		table.insert(notify_tags, "[HSV " ..  hsvmult .. "]");
	end

	-- Make a note if the heal type is only temporary hit points
	if healtype == "temp" then
		table.insert(notify_tags, "[TEMP]");
	end

	-- GET EFFECT MODIFIERS (ONLY FOR HEALING, NOT TEMPORARY HIT POINTS) (ALSO, MAKE SURE WE HAVE DICE OR HEALING SURGE VALUE)
	if healtype == "" and ((#dice > 0) or (hsvmult > 0)) then
		if rCreature then
			local bEffects = false;
			
			local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"HEAL"});
			if (nEffectCount > 0) then
				bEffects = true;
			end

			if bEffects then
				-- ADD IN THE MODIFIERS
				for k,v in pairs(eff_dice) do
					table.insert(dice, v);
				end
				mod = mod + eff_mod;

				-- ADD THE EFFECTS NOTIFICATION TAG
				local sEffects = "";
				local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
				if sMod ~= "" then
					sEffects = "[EFFECTS " .. sMod .. "]";
				else
					sEffects = "[EFFECTS]";
				end
				table.insert(notify_tags, sEffects);
			end
		end
	end
	
	-- BUILD THE OUTPUT
	local s = "[HEAL";
	if rAbility.order and rAbility.order > 1 then
		s = s .. " #" .. rAbility.order;
	end
	s = s .. "]";

	-- ADD THE NAME
	s = s .. " " .. rAbility.name;

	-- ADD ALL OTHER TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildSkillRoll(rCreature, rSkill)
	-- SETUP
	local dice = { "d20" };
	local mod = rSkill.mod;
	local notify_tags = {};
	
	-- IS ASSIST?
	local bAssist = Input.isShiftPressed();
	if bAssist then
		table.insert(notify_tags, "[ASSIST]");
	end

	-- GET EFFECT MODIFIERS
	local eff_dice = {};
	local eff_mod = 0;
	if rCreature then
		-- SETUP
		local bEffects = false;

		-- GET DIRECT SKILL MODIFIERS
		local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"SKILL"}, false, { rSkill.stat, rSkill.name });
		if (nEffectCount > 0) then
			bEffects = true;
		end
		
		-- GET CONDITION MODIFIERS
		if rSkill.name == "Perception" then
			if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Blinded") then
				eff_mod = eff_mod - 10;
				bEffects = true;
			end
			if EffectsManager.hasEffectCondition(rCreature.nodeCT, "Deafened") then
				eff_mod = eff_mod - 10;
				bEffects = true;
			end
		end
		
		if bEffects then
			-- ADD IN THE MODIFIERS
			for k,v in pairs(eff_dice) do
				table.insert(dice, v);
			end
			mod = mod + eff_mod;

			-- ADD THE EFFECTS NOTIFICATION TAG
			local sEffects = "";
			local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
			if sMod ~= "" then
				sEffects = "[EFFECTS " .. sMod .. "]";
			else
				sEffects = "[EFFECTS]";
			end
			table.insert(notify_tags, sEffects);
		end
	end

	-- BUILD THE OUTPUT
	local s = "[SKILL] " .. rSkill.name;
	
	-- ADD NOTIFICATION TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildAbilityCheckRoll(rCreature, rAbilityCheck)
	-- SETUP
	local dice = { "d20" };
	local notify_tags = {};
	
	-- DETERMINE MODIFIER
	local mod = getAbilityBonus(rCreature, rAbilityCheck.stat);
	mod = mod + getAbilityBonus(rCreature, "halflevel");
	
	-- GET EFFECT MODIFIERS
	if rCreature then
		local bEffects = false;

		local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"ABIL"}, false, { rAbilityCheck.stat });
		if (nEffectCount > 0) then
			bEffects = true;
		end

		if bEffects then
			-- ADD IN THE MODIFIERS
			for k,v in pairs(eff_dice) do
				table.insert(dice, v);
			end
			mod = mod + eff_mod;

			-- ADD THE EFFECTS NOTIFICATION TAG
			local sEffects = "";
			local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
			if sMod ~= "" then
				sEffects = "[EFFECTS " .. sMod .. "]";
			else
				sEffects = "[EFFECTS]";
			end
			table.insert(notify_tags, sEffects);
		end
	end

	-- BUILD THE OUTPUT
	local s = "[ABILITY]";
	s = s .. " " .. string.upper(string.sub(rAbilityCheck.stat, 1, 1)) .. string.sub(rAbilityCheck.stat, 2);
	s = s .. " check";
	
	-- ADD NOTIFICATION TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildInitRoll(rCreature, rInit)
	-- SETUP
	local dice = { "d20" };
	local mod = rInit.mod;
	local notify_tags = {};
	
	-- GET EFFECT MODIFIERS
	if rCreature then
		local bEffects = false;

		local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"INIT"});
		if (nEffectCount > 0) then
			bEffects = true;
		end

		if bEffects then
			-- ADD IN THE MODIFIERS
			for k,v in pairs(eff_dice) do
				table.insert(dice, v);
			end
			mod = mod + eff_mod;

			-- ADD THE EFFECTS NOTIFICATION TAG
			local sEffects = "";
			local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
			if sMod ~= "" then
				sEffects = "[EFFECTS " .. sMod .. "]";
			else
				sEffects = "[EFFECTS]";
			end
			table.insert(notify_tags, sEffects);
		end
	end

	-- BUILD THE OUTPUT
	local s = "Initiative";
	
	-- ADD NOTIFICATION TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

function buildSaveRoll(rCreature, rSave, nDeathSave)
	-- SETUP
	local dice = { "d20" };
	local mod = rSave.mod;
	local notify_tags = {};
	
	-- DEATH SAVE SETUP
	local subtypes = nil;
	if nDeathSave then
		if nDeathSave == 1 then
			table.insert(notify_tags, "[DEATH]");
			subtypes = { "death" };
		end
	elseif Input.isShiftPressed() then
		table.insert(notify_tags, "[DEATH]");
		subtypes = { "death" };
	end
	
	-- GET EFFECT MODIFIERS
	if rCreature then
		local bEffects = false;

		local eff_dice, eff_mod, nEffectCount = EffectsManager.getEffectsBonus(rCreature.nodeCT, {"SAVE"}, false, subtypes);
		if (nEffectCount > 0) then
			bEffects = true;
		end

		if bEffects then
			-- ADD IN THE MODIFIERS
			for k,v in pairs(eff_dice) do
				table.insert(dice, v);
			end
			mod = mod + eff_mod;

			-- ADD THE EFFECTS NOTIFICATION TAG
			local sEffects = "";
			local sMod = StringManager.convertDiceToString(eff_dice, eff_mod, true);
			if sMod ~= "" then
				sEffects = "[EFFECTS " .. sMod .. "]";
			else
				sEffects = "[EFFECTS]";
			end
			table.insert(notify_tags, sEffects);
		end
	end

	-- BUILD THE OUTPUT
	local s = "[SAVE] Saving Throw";
	
	-- ADD GM TAG
	if rSave.gmflag then
		s = "[GM] " .. s;
	end

	-- ADD NOTIFICATION TAGS
	if #notify_tags > 0 then
		s = s .. " " .. table.concat(notify_tags, " ");
	end
	
	-- RESULTS
	return s, dice, mod;
end

--
-- DAMAGE APPLICATION
--

function getDamageAdjust(rActor, nDamage, rDamageOutput, rFilterActor)
	-- SETUP
	local nDamageAdjust = 0;
	local bVulnerable = false;
	local bResist = false;
	local nHalf = 0;
	
	-- GET THE DAMAGE ADJUSTMENT EFFECTS
	local aImmune = EffectsManager.getEffectsBonusByType(rActor.nodeCT, "IMMUNE", false, rFilterActor);
	local aVuln = EffectsManager.getEffectsBonusByType(rActor.nodeCT, "VULN", false, rFilterActor);
	local aResist = EffectsManager.getEffectsBonusByType(rActor.nodeCT, "RESIST", false, rFilterActor);
	
	-- ADD ANY EFFECT MODIFIERS TO IMMUNE/VULN/RESIST
	-- EFFECT [PERTRIFIED] - GAIN RESIST 20
	if EffectsManager.hasEffectCondition(rActor.nodeCT, "Petrified") then
		if aResist[""] then
			aResist[""].mod = math.max((aResist[""].mod or 0), 20);
		else
			aResist[""] = {dice = {}, mod = 20};
		end
	end
	
	-- IF IMMUNE ALL, THEN JUST HANDLE IT NOW
	if aImmune["all"] then
		nDamageAdjust = 0 - nDamage;
		bResist = true;
		return nDamageAdjust, bVulnerable, bResist;
	end

	-- IF IMMUNE TO ENERGY TYPE, THEN DO NOT PROCESS VULNERABLE/RESIST TO THAT TYPE
	for k, v in pairs(aImmune) do
		aVuln[k] = nil;
		aResist[k] = nil;
	end
	
	-- ITERATE THROUGH EACH DAMAGE TYPE ENTRY
	for k, v in pairs(rDamageOutput.aDamageTypes) do
		-- GET THE INDIVIDUAL DAMAGE TYPES FOR THIS ENTRY (EXCLUDING UNTYPED DAMAGE TYPE)
		local aSrcDmgClauseTypes = {};
		local aTemp = StringManager.split(k, ",", true);
		for i = 1, #aTemp do
			if aTemp[i] ~= "untyped" and aTemp[i] ~= "" then
				table.insert(aSrcDmgClauseTypes, aTemp[i]);
			end
		end

		-- MAKE SURE THERE IS A DAMAGE TYPE LEFT
		if #aSrcDmgClauseTypes > 0 then
			-- CHECK IMMUNITY
			local nSourceImmune = 0;
			for keyDmgType, sDmgType in pairs(aSrcDmgClauseTypes) do
				if aImmune[sDmgType] then
					nSourceImmune = nSourceImmune + 1;
				end
			end
			
			-- IF IMMUNE, THEN DISCOUNT ALL OF THIS DAMAGE
			if nSourceImmune == #aSrcDmgClauseTypes then
				nDamageAdjust = nDamageAdjust - v;

			-- OTHERWISE, CHECK VULNERABILITY AND RESISTANCE
			else
				-- IF VULNERABLE TO ANY OF THE DAMAGE TYPES, THEN APPLY THE VULNERABILITY ONCE
				for keyDmgType, sDmgType in pairs(aSrcDmgClauseTypes) do
					if aVuln[sDmgType] and not aVuln[sDmgType].nApplied then
						aVuln[sDmgType].nApplied = aVuln[sDmgType].mod;
						nDamageAdjust = nDamageAdjust + aVuln[sDmgType].mod;
						bVulnerable = true;
					end
				end
				
				-- IF RESISTANT TO ALL OF THE DAMAGE TYPES, THEN RESIST UP TO THE AMOUNT OF THIS CLAUSE
				local nSourceResist = nil;
				for keyDmgType, sDmgType in pairs(aSrcDmgClauseTypes) do
					if aResist[sDmgType] then
						local nRemainingResist = aResist[sDmgType].mod - (aResist[sDmgType].nApplied or 0);
						if nRemainingResist > 0 then
							if nSourceResist then
								nSourceResist = math.min(nSourceResist, nRemainingResist);
							else
								nSourceResist = nRemainingResist;
							end
						else
							nSourceResist = nil;
							break;
						end
					else
						nSourceResist = nil;
						break;
					end
				end
				if nSourceResist then
					if nSourceResist > v then
						nSourceResist = v;
					end
					for keyDmgType, sDmgType in pairs(aSrcDmgClauseTypes) do
						aResist[sDmgType].nApplied = nSourceResist;
					end
					nDamageAdjust = nDamageAdjust - nSourceResist;
					bResist = true;
				end
			end
		end
	end

	-- HANDLE VULNERABILITY AND RESISTANCE TO ALL DAMAGE
	if aVuln[""] then
		nDamageAdjust = nDamageAdjust + aVuln[""].mod;
		bVulnerable = true;
	end
	if aResist[""] then
		nDamageAdjust = nDamageAdjust - aResist[""].mod;
		bResist = true;
	end

	-- HANDLE EFFECT MODIFIERS
	if (nDamage + nDamageAdjust > 0) then
		-- [EFFECT] SWARM - HALF DAMAGE VS. M/R ATTACKS, EXTRA DAMAGE VS. C/A ATTACKS)
		local nSwarm = EffectsManager.getEffectsBonus(rActor.nodeCT, "SWARM", true);
		if nSwarm > 0 then
			if rDamageOutput.sRange == "M" or rDamageOutput.sRange == "R" then
				nHalf = nHalf + 1;
			elseif rDamageOutput.sRange == "C" or rDamageOutput.sRange == "A" then
				nDamageAdjust = nDamageAdjust + nSwarm;
				bVulnerable = true;
			end
		end
		
		-- [EFFECT] INSUBSTANTIAL - HALF DAMAGE FROM ALL SOURCES
		if EffectsManager.hasEffectCondition(rActor.nodeCT, "Insubstantial") then
			nHalf = nHalf + 1;
		end
		
		-- IF HALF DAMAGE SET BY ATTACKER WEAKENED STATE, APPLY ANOTHER HALF DAMAGE INCREMENT
		if rDamageOutput.nHalf > 0 then
			nHalf = nHalf + 1;
		end
		
		-- APPLY HALF DAMAGE
		for i = 1, nHalf do
			nDamageAdjust = nDamageAdjust - math.ceil((nDamage + nDamageAdjust) / 2);
		end
		if (nDamage + nDamageAdjust < 1) then
			nDamageAdjust = 0 - (nDamage - 1);
		end
	end

	-- RESULTS
	return nDamageAdjust, bVulnerable, bResist, nHalf;
end

function decodeDamageText(nDamage, sDamageDesc)
	local rDamageOutput = {};
	rDamageOutput.sType = "damage";
	rDamageOutput.sTypeOutput = "Damage";
	rDamageOutput.sVal = "" .. nDamage;
	rDamageOutput.nVal = nDamage;
	
	if string.match(sDamageDesc, "%[HEAL") then
		rDamageOutput.nHealCost = tonumber(string.match(sDamageDesc, "%[COST ([%+%-]?%d+)%]")) or 0;
		rDamageOutput.nHSVMult = tonumber(string.match(sDamageDesc, "%[HSV ([%+%-]?%d+)]")) or 0;
		
		rDamageOutput.sVal = "";		
		if rDamageOutput.nHSVMult > 0 then
			if rDamageOutput.nHSVMult > 1 then
				rDamageOutput.sVal = rDamageOutput.sVal .. rDamageOutput.nHSVMult .. "*";
			end
			rDamageOutput.sVal = rDamageOutput.sVal .. "HSV";
			if nDamage ~= 0 then
				rDamageOutput.sVal = rDamageOutput.sVal .. "+" .. nDamage;
			end
		else
			rDamageOutput.sVal = "" .. nDamage;
		end

		if string.match(sDamageDesc, "%[TEMP%]") then
			-- SET MESSAGE TYPE
			rDamageOutput.sType = "temphp";
			rDamageOutput.sTypeOutput = "Temporary hit points";
		else
			-- SET MESSAGE TYPE
			rDamageOutput.sType = "heal";
			rDamageOutput.sTypeOutput = "Heal";
		end
	elseif nDamage < 0 then
		rDamageOutput.sType = "heal";
		rDamageOutput.sTypeOutput = "Heal";
		rDamageOutput.nHealCost = 0;
		rDamageOutput.nHSVMult = 0;
		rDamageOutput.sVal = "" .. (0 - nDamage);
		rDamageOutput.nVal = 0 - nDamage;
	else
		-- DETERMINE RANGE
		rDamageOutput.sRange = string.match(sDamageDesc, "%[DAMAGE %((%w)%)%]") or "";

		-- DETERMINE DAMAGE ENERGY TYPES
		rDamageOutput.aDamageTypes = {};
		local nDamageRemaining = nDamage;
		for sDamageType in string.gmatch(sDamageDesc, "%[TYPE: ([^%]]+)%]") do
			local sEnergyType = StringManager.trimString(string.match(sDamageType, "^([^(%]]+)"));
			local sDice, sTotal = string.match(sDamageType, "%(([%d%+%-D]+)%=(%d+)%)");
			local nEnergyTypeTotal = tonumber(sTotal) or nDamageRemaining;

			if rDamageOutput.aDamageTypes[sEnergyType] then
				rDamageOutput.aDamageTypes[sEnergyType] = dmgtypes[sEnergyType] + nEnergyTypeTotal;
			else
				rDamageOutput.aDamageTypes[sEnergyType] = nEnergyTypeTotal;
			end

			nDamageRemaining = nDamageRemaining - nEnergyTypeTotal;
			if nDamageRemaining <= 0 then
				break;
			end
		end
		if nDamageRemaining > 0 then
			rDamageOutput.aDamageTypes[""] = nDamageRemaining;
		end
		
		-- DETERMINE DAMAGE TYPES
		rDamageOutput.aDamageFilter = {};
		if rDamageOutput.sRange == "M" then
			table.insert(rDamageOutput.aDamageFilter, "melee");
		elseif rDamageOutput.sRange == "R" then
			table.insert(rDamageOutput.aDamageFilter, "ranged");
		elseif rDamageOutput.sRange == "C" then
			table.insert(rDamageOutput.aDamageFilter, "close");
		elseif rDamageOutput.sRange == "A" then
			table.insert(rDamageOutput.aDamageFilter, "area");
		end
		
		-- DETERMINE WHETHER HALF DAMAGE SHOULD BE APPLIED FROM ATTACKER EFFECTS
		rDamageOutput.nHalf = 0;
		if string.match(sDamageDesc, "%[HALF%]") then
			rDamageOutput.nHalf = rDamageOutput.nHalf + 1;
		end
	end
	
	return rDamageOutput;
end

function applyDamage(rActor, dmgval, dmgstr, rAttacker)
	-- SETUP
	local totalhp = 0;
	local temphp = 0;
	local wounds = 0;
	local nSurgeMax = 0;
	local nSurgeUsed = 0;
	local nHSV = 0;

	local aNotifications = {};
	
	-- GET HEALTH FIELDS
	if rActor.sType == "pc" and rActor.nodeCreature then
		totalhp = NodeManager.get(rActor.nodeCreature, "hp.total", 0);
		temphp = NodeManager.get(rActor.nodeCreature, "hp.temporary", 0);
		wounds = NodeManager.get(rActor.nodeCreature, "hp.wounds", 0);
		nSurgeMax = NodeManager.get(rActor.nodeCreature, "hp.surgesmax", 0);
		nSurgeUsed = NodeManager.get(rActor.nodeCreature, "hp.surgesused", 0);
		nHSV = NodeManager.get(rActor.nodeCreature, "hp.surge", 0);
	elseif rActor.nodeCT then
		totalhp = NodeManager.get(rActor.nodeCT, "hp", 0);
		temphp = NodeManager.get(rActor.nodeCT, "hptemp", 0);
		wounds = NodeManager.get(rActor.nodeCT, "wounds", 0);

		if NodeManager.get(rActor.nodeCT, "type", "") == "pc" then
			nSurgeMax = NodeManager.get(rActor.nodeCT, "healsurgesmax", 0);
			nSurgeUsed = NodeManager.get(rActor.nodeCT, "healsurgesused", 0);
			nHSV = NodeManager.get(rActor.nodeCT, "healsurgeval", 0);
		else
			nSurgeMax = NodeManager.get(rActor.nodeCT, "healsurgeremaining", 0);
			nSurgeUsed = 0;
			nHSV = math.floor(totalhp / 4);
		end
	else
		return "";
	end
	
	-- DECODE DAMAGE DESCRIPTION
	local rDamageOutput = decodeDamageText(dmgval, dmgstr);
	
	-- HEALING
	if rDamageOutput.sType == "heal" then
		-- CHECK COST
		local bWounded = (wounds > 0);
		local bApplyHealing = true;
		if rDamageOutput.nHealCost < 0 then
			if (rDamageOutput.nHealCost + nSurgeUsed) < 0 then
				table.insert(aNotifications, "[ALREADY AT MAX HEAL SURGES (+" .. (0 - (rDamageOutput.nHealCost + nSurgeUsed)) .. ")]");
			end
		elseif rDamageOutput.nHealCost > 0 then
			local nSurgesAvailable = nSurgeMax - nSurgeUsed;
			if rDamageOutput.nHealCost > nSurgesAvailable then
				table.insert(aNotifications, "[INSUFFICIENT HEAL SURGES (-" .. (rDamageOutput.nHealCost - nSurgesAvailable) .. ")]");

				-- IF THIS IS REALLY A HEAL AND NOT HEALING SURGE DAMAGE, THEN DON'T APPLY HEALING
				if rDamageOutput.nHSVMult ~= 0 or rDamageOutput.nVal ~= 0 then
					bApplyHealing = false;
				end
			end

			-- IF THE TARGET IS NOT WOUNDED, THEN DO NOT APPLY HEALING.
			if bApplyHealing and not bWounded and ((rDamageOutput.nHSVMult > 0) or (rDamageOutput.nVal > 0)) then
				table.insert(aNotifications, "[NOT WOUNDED]");
				bApplyHealing = false;
			end
		end
			
		if bApplyHealing then
			-- APPLY HEAL COST
			nSurgeUsed = nSurgeUsed + rDamageOutput.nHealCost;
			if nSurgeUsed > nSurgeMax then
				nSurgeUsed = nSurgeMax;
			elseif nSurgeUsed < 0 then
				nSurgeUsed = 0;
			end

			-- APPLY HEALING
			if wounds > totalhp then
				wounds = totalhp
			end
			wounds = wounds - (rDamageOutput.nVal + (rDamageOutput.nHSVMult * nHSV));
			if wounds < 0 then
				wounds = 0;
			end
		end

	-- TEMPORARY HIT POINTS
	elseif rDamageOutput.sType == "temphp" then
		-- APPLY TEMPORARY HIT POINTS
		dmgval = dmgval + (rDamageOutput.nHSVMult * nHSV);
		temphp = math.max(temphp, dmgval);

	-- DAMAGE
	else
		-- APPLY ANY TARGETED DAMAGE EFFECTS
		-- NOTE: DICE ARE RANDOMLY DETERMINED BY COMPUTER, INSTEAD OF ROLLED
		if rAttacker then
			local isCritical = string.match(dmgstr, "%[CRITICAL%]");
			local aTargetedDamage = EffectsManager.getEffectsBonusByType(rAttacker.nodeCT, {"DMG"}, true, rDamageOutput.aDamageFilter, rActor, true);

			local nDamageEffectTotal = 0;
			local nDamageEffectCount = 0;
			for k, v in pairs(aTargetedDamage) do
				local nSubTotal = StringManager.evalDice(v.dice, v.mod, isCritical);
				rDamageOutput.aDamageTypes[k] = (rDamageOutput.aDamageTypes[k] or 0) + nSubTotal;
				nDamageEffectTotal = nDamageEffectTotal + nSubTotal;
				nDamageEffectCount = nDamageEffectCount + 1;
			end
			dmgval = dmgval + nDamageEffectTotal;

			-- CAPTURE ANY TARGETED WEAKNESS EFFECTS
			if rDamageOutput.nHalf == 0 then
				if rAttacker.nodeCT and rActor.nodeCT then
					if EffectsManager.hasEffect(rAttacker.nodeCT, "Weakened", rActor.nodeCT, true) then
						rDamageOutput.nHalf = 1;
					end
				end
			end
			
			if nDamageEffectCount > 0 then
				if nDamageEffectTotal ~= 0 then
					table.insert(aNotifications, string.format("[EFFECTS %+d]", nDamageEffectTotal));
				else
					table.insert(aNotifications, "[EFFECTS]");
				end
			end
		end
		
		-- APPLY ANY DAMAGE TYPE ADJUSTMENT EFFECTS
		local nDamageAdjust, bVulnerable, bResist, nHalf = getDamageAdjust(rActor, dmgval, rDamageOutput, rAttacker);

		-- ADDITIONAL DAMAGE ADJUSTMENTS NOT RELATED TO DAMAGE TYPE
		local nAdjustedDamage = dmgval + nDamageAdjust;
		if nAdjustedDamage < 0 then
			nAdjustedDamage = 0;
		end
		if bResist then
			if nAdjustedDamage <= 0 then
				table.insert(aNotifications, "[RESISTED]");
			else
				table.insert(aNotifications, "[PARTIALLY RESISTED]");
			end
		end
		if bVulnerable then
			table.insert(aNotifications, "[VULNERABLE]");
		end
		if nHalf > 0 then
			if nHalf > 1 then
				table.insert(aNotifications, string.format("[HALF (x%d)]", nHalf));
			else
				table.insert(aNotifications, "[HALF]");
			end
		end
		
		-- REDUCE DAMAGE BY TEMPORARY HIT POINTS
		if temphp > 0 then
			if nAdjustedDamage > temphp then
				nAdjustedDamage = nAdjustedDamage - temphp;
				temphp = 0;
				table.insert(aNotifications, "[PARTIALLY ABSORBED]");
			else
				temphp = temphp - nAdjustedDamage;
				nAdjustedDamage = 0;
				table.insert(aNotifications, "[ABSORBED]");
			end
		end

		-- APPLY REMAINING DAMAGE
		local nOriginalWounds = wounds;
		wounds = wounds + nAdjustedDamage;
		if wounds < 0 then
			wounds = 0;
		end

		-- ADD STATUS CHANGE NOTIFICATIONS
		local bloodied = totalhp / 2;
		if (nOriginalWounds < totalhp) and (wounds >= totalhp) then
			table.insert(aNotifications, "[DYING]");
		elseif (nOriginalWounds < bloodied) and (wounds >= bloodied) then
			table.insert(aNotifications, "[BLOODIED]");
		end

		-- Update the damage output variable to reflect adjustments
		rDamageOutput.nVal = nAdjustedDamage;
		rDamageOutput.sVal = "" .. nAdjustedDamage;
	end

	-- SET HEALTH FIELDS
	if rActor.sType == "pc" and rActor.nodeCreature then
		NodeManager.set(rActor.nodeCreature, "hp.temporary", "number", temphp);
		NodeManager.set(rActor.nodeCreature, "hp.wounds", "number", wounds);
		NodeManager.set(rActor.nodeCreature, "hp.surgesused", "number", nSurgeUsed);
	else
		NodeManager.set(rActor.nodeCT, "hptemp", "number", temphp);
		NodeManager.set(rActor.nodeCT, "wounds", "number", wounds);

		if NodeManager.get(rActor.nodeCT, "type", "") == "pc" then
			NodeManager.set(rActor.nodeCT, "healsurgesused", "number", nSurgeUsed);
		else
			NodeManager.set(rActor.nodeCT, "healsurgeremaining", "number", nSurgeMax - nSurgeUsed);
		end
	end

	-- OUTPUT RESULTS
	local bGMOnly = string.match(dmgstr, "^%[GM%]") or string.match(dmgstr, "^%[TOWER%]") ;
	ChatManager.messageDamage(bGMOnly, rDamageOutput.sTypeOutput, rDamageOutput.sVal, rActor, table.concat(aNotifications, " "));
end

--
--  GENERAL
--

function getAbilityBonus(rActor, sAbility)
	-- VALIDATE
	if not rActor or not rActor.nodeCreature or not sAbility then
		return 0;
	end
	
	-- SETUP
	local sStat = sAbility;
	local bHalf = false;
	local bDouble = false;
	local nStatVal = 0;
	
	-- HANDLE HALF/DOUBLE MODIFIERS
	if string.match(sStat, "^half") then
		bHalf = true;
		sStat = string.sub(sStat, 5);
	end
	if string.match(sStat, "^double") then
		bDouble = true;
		sStat = string.sub(sStat, 7);
	end

	-- GET ABILITY VALUE
	local shortname = string.sub(string.lower(sStat), 1, 3);
	if rActor.sType == "npc" then
		if shortname == "lev" then
			nStatVal = tonumber(string.match(NodeManager.get(rActor.nodeCreature, "levelrole", ""), "Level (%d*)")) or 0;
		else
			if shortname == "str" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "strength", 0);
				nStatVal = math.floor((nStatVal - 10) / 2);
			elseif shortname == "dex" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "dexterity", 0);
				nStatVal = math.floor((nStatVal - 10) / 2);
			elseif shortname == "con" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "constitution", 0);
				nStatVal = math.floor((nStatVal - 10) / 2);
			elseif shortname == "int" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "intelligence", 0);
				nStatVal = math.floor((nStatVal - 10) / 2);
			elseif shortname == "wis" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "wisdom", 0);
				nStatVal = math.floor((nStatVal - 10) / 2);
			elseif shortname == "cha" then
				nStatVal = NodeManager.get(rActor.nodeCreature, "charisma", 0);
			end
		end
	elseif rActor.sType == "pc" then
		if shortname == "str" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.strength.bonus", 0);
		elseif shortname == "dex" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.dexterity.bonus", 0);
		elseif shortname == "con" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.constitution.bonus", 0);
		elseif shortname == "int" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.intelligence.bonus", 0);
		elseif shortname == "wis" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.wisdom.bonus", 0);
		elseif shortname == "cha" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "abilities.charisma.bonus", 0);
		elseif shortname == "lev" then
			nStatVal = NodeManager.get(rActor.nodeCreature, "level", 0);
		end
	end
	
	-- APPLY HALF/DOUBLE MODIFIERS
	if bDouble then
		nStatVal = nStatVal * 2;
	end
	if bHalf then
		nStatVal = math.floor(nStatVal / 2);
	end

	-- RESULTS
	return nStatVal;
end

function getDefenseInternal(sDefense)
	local sInternal = "";
	
	if sDefense then
		local sShortDef = string.sub(string.lower(sDefense), 1, 2);
		if sShortDef == "ac" then
			sInternal = "ac";
		elseif sShortDef == "fo" then
			sInternal = "fortitude";
		elseif sShortDef == "re" then
			sInternal = "reflex";
		elseif sShortDef == "wi" then
			sInternal = "will";
		end
	end
	
	return sInternal;
end

function getDefenseValue(rAttacker, rDefender, sAttack)
	-- VALIDATE
	if not rDefender or not sAttack then
		return nil, 0, 0;
	end

	-- DETERMINE ATTACK TYPE AND DEFENSE
	local sAttackType = string.match(sAttack, "%[ATTACK.*%((%w+)%)%]");
	local bOpportunity = string.match(sAttack, "%[OPPORTUNITY%]");
	local sDefense = string.match(sAttack, "%(vs%.? (%a+)%)");
	if not sDefense then
		return nil, 0, 0;
	end
	sDefense = getDefenseInternal(sDefense);
	if sDefense == "" then
		return nil, 0, 0;
	end

	-- Determine the defense database node name
	local nDefense = 10;
	if rDefender.nodeCT then
		nDefense = NodeManager.get(rDefender.nodeCT, sDefense, 10);
	elseif rDefender.nodeCreature then
		if rDefender.sType == "pc" then
			nDefense = NodeManager.get(rDefender.nodeCreature, "defenses." .. sDefense .. ".total", 10);
		else
			nDefense = NodeManager.get(rDefender.nodeCreature, sDefense, 10);
		end
	else
		return nil, 0, 0;
	end

	-- EFFECT MODIFIERS
	local nAttackEffectMod = 0;
	local nDefenseEffectMod = 0;
	if rDefender.nodeCT then
		-- SETUP
		local bCombatAdvantage = false;
		local bUncannyDodge = false;
		local bCheckCA = true;
		local nBonusAllDefenses = 0;
		local nBonusSpecificDefense = 0;
		local nBonusSituational = 0;
		local nodeCTAttacker = nil;
		if rAttacker then
			nodeCTAttacker = rAttacker.nodeCT;
		end
		
		-- BUILD ATTACK FILTER 
		local aAttackFilter = {};
		if sAttackType == "M" then
			table.insert(aAttackFilter, "melee");
		elseif sAttackType == "R" then
			table.insert(aAttackFilter, "ranged");
		elseif sAttackType == "C" then
			table.insert(aAttackFilter, "close");
		elseif sAttackType == "A" then
			table.insert(aAttackFilter, "area");
		end
		if bOpportunity then
			table.insert(aAttackFilter, "opportunity");
		end

		-- GET ATTACKER BASE MODIFIER
		local aBonusTargetedAttackDice, nBonusTargetedAttack = EffectsManager.getEffectsBonus(nodeCTAttacker, "ATK", false, aAttackFilter, rDefender, true);
		nAttackEffectMod = nAttackEffectMod + StringManager.evalDice(aBonusTargetedAttackDice, nBonusTargetedAttack);
			
		-- GET ATTACKER SITUATIONAL MODIFIERS
		-- AND CHECK WHETHER COMBAT ADVANTAGE HAS ALREADY BEEN APPLIED
		if EffectsManager.hasEffectCondition(rDefender.nodeCT, "Uncanny Dodge") then
			bUncannyDodge = true;
			bCheckCA = false;
		end
		if EffectsManager.hasEffect(nodeCTAttacker, "CA") or EffectsManager.hasEffect(nodeCTAttacker, "Invisible") then
			bCheckCA = false;
			if bUncannyDodge then
				nBonusSituational = nBonusSituational + 2;
			end
		end
		if bCheckCA then
			if EffectsManager.hasEffect(nodeCTAttacker, "CA", rDefender.nodeCT, true) or 
					EffectsManager.hasEffect(nodeCTAttacker, "Invisible", rDefender.nodeCT, true) then
				bCombatAdvantage = true;
			end
		end
		
		-- GET DEFENDER ALL DEFENSE MODIFIERS
		nBonusAllDefenses = EffectsManager.getEffectsBonus(rDefender.nodeCT, "DEF", true, aAttackFilter, rAttacker);
		
		-- GET DEFENDER SPECIFIC DEFENSE MODIFIERS
		if sDefense == "ac" then
			nBonusSpecificDefense = EffectsManager.getEffectsBonus(rDefender.nodeCT, "AC", true, aAttackFilter, rAttacker);
		elseif sDefense == "fortitude" then
			nBonusSpecificDefense = EffectsManager.getEffectsBonus(rDefender.nodeCT, "FORT", true, aAttackFilter, rAttacker);
		elseif sDefense == "reflex" then
			nBonusSpecificDefense = EffectsManager.getEffectsBonus(rDefender.nodeCT, "REF", true, aAttackFilter, rAttacker);
		elseif sDefense == "will" then
			nBonusSpecificDefense = EffectsManager.getEffectsBonus(rDefender.nodeCT, "WILL", true, aAttackFilter, rAttacker);
		end
		
		-- GET DEFENDER SITUATIONAL MODIFIERS - COMBAT ADVANTAGE
		if bCheckCA then
			-- CHECK ALL THE CONDITIONS THAT COULD GRANT COMBAT ADVANTAGE
			if EffectsManager.hasEffect(rDefender.nodeCT, "GRANTCA", nodeCTAttacker) then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Blinded") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Dazed") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Dominated") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Helpless") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Prone") and (sAttackType == "M") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Restrained") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Stunned") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Surprised") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Unconscious") then
				bCombatAdvantage = true;
			end
			
			-- CHECK ALL THE OTHER EFFECTS THAT COULD GRANT COMBAT ADVANTAGE
			if EffectsManager.hasEffectCondition(rDefender.nodeCT, "Balancing") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Climbing") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Running") then
				bCombatAdvantage = true;
			elseif EffectsManager.hasEffectCondition(rDefender.nodeCT, "Squeezing") then
				bCombatAdvantage = true;
			end

			-- APPLY COMBAT ADVANTAGE AS DEFENSE PENALTY
			if bCombatAdvantage then
				nBonusSituational = nBonusSituational - 2;
			end
		end
		
		-- GET DEFENDER SITUATIONAL MODIFIERS - CONDITIONS
		if EffectsManager.hasEffectCondition(rDefender.nodeCT, "Prone") and (sAttackType == "R") then
			nBonusSituational = nBonusSituational + 2;
		end
		if EffectsManager.hasEffectCondition(rDefender.nodeCT, "Unconscious") then
			nBonusSituational = nBonusSituational - 5;
		end
		
		-- GET DEFENDER SITUATIONAL MODIFIERS - CONCEALMENT/COVER
		if sAttackType and (sAttackType == "M" or sAttackType == "R") then
			if EffectsManager.hasEffect(rDefender.nodeCT, "Invisible", nodeCTAttacker) then
				nBonusSituational = nBonusSituational + 5;
			else
				local aConcealment = EffectsManager.getEffectsByType(rDefender.nodeCT, "TCONC", aAttackFilter, rAttacker);
				if #aConcealment > 0 or EffectsManager.hasEffect(rDefender.nodeCT, "TCONC", nodeCTAttacker) then
					nBonusSituational = nBonusSituational + 5;
				else
					aConcealment = EffectsManager.getEffectsByType(rDefender.nodeCT, "CONC", aAttackFilter, rAttacker);
					if #aConcealment > 0 or EffectsManager.hasEffect(rDefender.nodeCT, "CONC", nodeCTAttacker) then
						nBonusSituational = nBonusSituational + 2;
					end
				end
			end
		end
		local aCover = EffectsManager.getEffectsByType(rDefender.nodeCT, "SCOVER", aAttackFilter, rAttacker);
		if #aCover > 0 or EffectsManager.hasEffect(rDefender.nodeCT, "SCOVER", nodeCTAttacker) then
			nBonusSituational = nBonusSituational + 5;
		else
			aCover = EffectsManager.getEffectsByType(rDefender.nodeCT, "COVER", aAttackFilter, rAttacker);
			if #aCover > 0 or EffectsManager.hasEffect(rDefender.nodeCT, "COVER", nodeCTAttacker) then
				nBonusSituational = nBonusSituational + 2;
			end
		end
		
		-- ADD IN EFFECT MODIFIERS
		nDefenseEffectMod = nBonusAllDefenses + nBonusSpecificDefense + nBonusSituational;
	end
	
	-- Return the final defense value
	return nDefense + nDefenseEffectMod - nAttackEffectMod, nAttackEffectMod, nDefenseEffectMod;
end

